# -*- coding: utf8 -*-
# !/usr/bin/env python


""" 文档说明 V0.1 : 
# 使用 Airtest - poco 框架, 来测试 七猫免费小说APP 应用(熟悉Airtest-Poco框架);
# 检索 novel name 中返回相关响应小说以及评分, 并模拟阅读浏览小说

@version : 0.1
@author  : Knight
@email   : xxxxxx@mail.com
@Created : 2023-05-08 10:15:12
@Project : QimaoFreeNovelApp - Airtest
@Module  : qimao_free_novel.py
@Modified: 
"""


import random
import time

from airtest.cli.parser import cli_setup
from airtest.core.api import *
from airtest.core.android import Android
# 使用 Windows Shell 的 API, 执行 shell 命令
from airtest.core.win import Windows
from airtest.report.report import simple_report

from poco.drivers.android.uiautomation import AndroidUiautomationPoco


if not cli_setup():
    auto_setup(__file__,
               logdir=True, 
               devices=["android://127.0.0.1:5037/127.0.0.1:7555?cap_method=JAVACAP&ori_method=ADBORI&touch_method=ADBTOUCH&",],
               compress=90)


# generate html report
simple_report(__file__, logpath=True)

# 设置全局的随机等待时长
global_random_delay = random.randrange(4, 10) * 0.5


# ---------------------------------------------------------------------------------------
# [1.1]默认：设备列表中, 默认选取首台连接设备
device = connect_device("android:///?cap_method=JAVACAP")
# [1.2] 关于带参数的连接方法
# device = connect_device("android://127.0.0.1:5037/127.0.0.1:7555?cap_method=JAVACAP&&ori_method=ADBORI")
# ---------------------------------------------------------------------------------------
# [1.3] 快速创建多台设备-实例化(推荐: 方法二), 适用于设备较多情形.
"""
    Tip01: 全局变量 G.DEVICE_LIST: 查看所有当前连接中的设备，
    Tip02: 使用 set_current(num or serialno) 在多台设备之间切换
    TIP03：device.get_display_info() # 查看当前设备屏幕参数信息
"""

# 参考代码如下:

# 方法一: 根据设备ID直接实例化多台设备实例(不推荐)
# device_1 = Android('76efadf3a7ce4')
# device_2 = Android('adfasdfasf23')
# device_3 = Android('adifu39ernla')

# poco_1 = AndroidUiautomationPoco(device_1, use_airtest_input=True, screenshot_each_action=False)
# poco_2 = AndroidUiautomationPoco(device_2, use_airtest_input=True, screenshot_each_action=False)
# poco_3 = AndroidUiautomationPoco(device_3, use_airtest_input=True, screenshot_each_action=False)

# 方法二: 创建多台设备实例(Tips: 可通过使用 adb shell 命令来获取当前主机连接设备名称)

def connect_multi_devices(default_host="127.0.0.1:5037", sep_symbol="-", sep_length=80):
    """ 建立与当前主机已连接的所有设备(通过 adb shell 命令方式)，并打印相关设备的信息(连接地址,
        屏幕参数信息等).
        @default_host: 默认主机连接地址 "127.0.0.1:5037"
        @sep_symbol: 分割符号
        @sep_length: 分割符号长度
        @return: dict 类型容器    
    """
    
    # 收集连接设备device, poco对象的容器
    container = dict()
    
    # 分割线
    sep_line = sep_symbol * sep_length
    
    # 通过 adb shell 获取与当前主机连接的所有Android设备信息
    conn_devices = Windows().shell("adb devices").decode("utf-8").strip()
    devices_list = [_dev.split("\t")[0] for _dev in conn_devices.split("\n")[1:]]
  
    # 模拟开始建立设别连接
    log_record_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    print(f"[{log_record_at}] 开始建立连接设备连接...\n{sep_line}")
    
    for _no, serialno in enumerate(devices_list, start=1):
        # 构建连接参数信息
        _device_no = f"device_{_no:03d}"
        conn_address = f"android://{default_host}/{serialno}?cap_method=JAVACAP&touch_method=MAXTOUCH&"

        # 建立连接，实例化Poco驱动
        log_record_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        connected_device = connect_device(conn_address)
        poco = AndroidUiautomationPoco(device=connected_device,
                                                 using_proxy=True, 
                                                 force_restart=False,
                                                 use_airtest_input=False,
                                                 screenshot_each_action=False)   
        # 收集 poco, device 实例对象
        container[_device_no] = {"poco": poco, "device": connected_device}
        
        # 获取屏幕参数信息(宽度,高度,密度，方向，旋转,分辨率等)
        screen_info = connected_device.get_display_info()
        
        # 打印相关设备信息
        log_record_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{log_record_at}] 设备编号: {_device_no}, 序列号: {serialno}, 屏幕信息: {screen_info}")
        print(sep_line)
        
    return container

def show_connected_device_infos():
    """ 展示与主机建立连接的所有设备信息"""

    print("展示与主机建立连接的所有设备信息: ")
    for _no, conn_device in enumerate(G.DEVICE_LIST, start=1):
        # 当前连接设备信息
        serialno = conn_device.__dict__.get("serialno")
        ip_address = conn_device.get_ip_address()

        # 打印设备信息
        print(f"序号: {_no:03d}, 设备序列号: {serialno}, 设备IP地址: {ip_address}")
# --------------------------------------------------------------------------------------
# [2]使用 poco 创建 Android 仿真模拟器
poco = AndroidUiautomationPoco(device, use_airtest_input=True, screenshot_each_action=False)

# ---------------------------------------------------------------------------------------
# 关于 App 操作 API: https://airtest.readthedocs.io/zh-cn/latest/all_module/airtest.core.android.html
# 针对 device 对象的常用方法，如下所示:
# [1] 启动 APP 应用程序: start_app(app_package)
# [2] 清理 App 缓存数据: clear_app(app_package)
# [3] 关闭 App 应用程序: stop_app(app_package)
# [4] 列出 App 程序列表: list_app(app_package)，Tip: third_only – 如果为True，返回所有三方应用列表
# [5] 安装 App 应用程序: install_app(app_package)
# [6] 卸载 App 应用程序: uninstall_app(app_package)
# [7] 检查 APP 是否存在: check_app(app_package)
# [8] 查看 APP 完整路径: path_app(app_package)
# [9] 查看 APP 启动耗时: start_app_timing(app_package, activity) 
# ---------------------------------------------------------------------------------------

# [3] 点击应用APP：七猫免费小说(使用文本Text定位)

# 唤醒设备
device.wake()

# 方法1：通过命令 adb shell pm list packages 查看所有安装包(包含Android系统内置应用)
# 使用方式: device.shell("pm list packages")
# 方法2：使用内置API, 查看所有设备安装包(参数third_only=True仅显示第三方包)
# 使用方式: device.list_app(third_only=True)

# 查看已安装第三方Apps
installed_apps = device.list_app(third_only=True)
print(installed_apps)

# App package
app_name = "七猫免费小说"
app_package = "com.kmxs.reader"

# 启动App
start_app(app_package)


# [3.1] APP 加载响应时间，设置 10s(通常5s内即可加载成功，考虑网络较差情形)
app_start_delay = random.randrange(10, 20) * 0.5
sleep(app_start_delay)

# [3.2] 点击搜索框(使用 name 定位) - 使用wait等待控件出现，并随机等待
poco(name="com.kmxs.reader:id/search_banner").wait(timeout=5.0).click()
click_search_banner_delay = random.randrange(2, 4) * 0.5
sleep(click_search_banner_delay)

# [3.3] 填写搜索框架(输入文本：斗罗大陆), 并查看响应小说列表
def search_banner_response(search_novel="斗罗大陆", sep_line=100, poco=poco):
    """ 打印检索 Banner 响应小说文本信息"""

    print("*" * sep_line)
    print(f"打印 Search Banner - {search_novel}响应信息:")

    # 输入检索小说文本
    poco(name="com.kmxs.reader:id/search_text").set_text(search_novel)
    wait_search_reponse_delay = random.randrange(2, 4) * 0.5
    sleep(wait_search_reponse_delay)

    novel_list = poco(name="com.kmxs.reader:id/search_think_result_name")
    for _no, show_novel in enumerate(novel_list, start=1):
        _novel = show_novel.get_text()
        print(f"Search novel: {search_novel}, Results: {_no:03d} - {_novel}")

    print("*" * sep_line)


# 检索响应信息
search_banner_response()

# [3.4] 查看搜索小说以及对应评分(利用 父子/孙子节点，分级定位)
# 点击搜索
poco(name="com.kmxs.reader:id/search_tv", textMatches="搜索").click()
sleep(global_random_delay)
# 选中 Banner 中 "书籍" 选项
poco(name="android.widget.TextView", text="书籍").click()


# 直接复制节点路径（右键点击即可）
# poco("com.kmxs.reader:id/swipe").child("android.widget.FrameLayout").child("android.widget.LinearLayout").offspring("android:id/content").child("android.widget.LinearLayout").offspring("com.kmxs.reader:id/search_activity_main_view").offspring("com.kmxs.reader:id/view_pager").child("android.widget.FrameLayout").child("android.widget.LinearLayout").offspring("com.kmxs.reader:id/search_result_view")


# 收集小说中书籍的相关列表
novels_list = list()

def random_swipe_down(random_swipe_delay=(1, 10), ratio=0.55):
    """ 设置随机滑动, 由下至上随机;
    @random_swipe_delay: 设置滑动后随机等待延时区间: [1, 10] 秒.
    @ratio: 模拟人工等待时长的干扰项.
    """
    
    begin_x = random.randrange(40, 60) * 0.01
    end_x = random.randrange(76, 85) * 0.01
    begin_y = random.randrange(40, 60) * 0.01
    end_y = random.randrange(26, 35) * 0.01
    
    # 随机滑动(指定范围内)
    poco.swipe([begin_x, end_x], [begin_y, end_y], duration=1.2)

    # 设置随机等待
    swipe_delay = random.randrange(*random_swipe_delay) * ratio
    sleep(swipe_delay)


while True:
    # Step01: 设置滑动页面停止条件(检索到文本："大家还在搜")
    stop_text_condition = "大家还在搜"
    
    # Step02: 收集统计数据
    novel_index = len(novels_list)

    # 获取父节点的绝对路径
    parent_path = poco("com.kmxs.reader:id/swipe").child("android.widget.FrameLayout").child("android.widget.LinearLayout").offspring("android:id/content").child("android.widget.LinearLayout").offspring("com.kmxs.reader:id/search_activity_main_view").offspring("com.kmxs.reader:id/view_pager").child("android.widget.FrameLayout").child("android.widget.LinearLayout").offspring("com.kmxs.reader:id/search_result_view")

    search_paths = parent_path.child("com.kmxs.reader:id/book_store_select_book").child("android.widget.LinearLayout").child("android.view.ViewGroup")
    
    if poco(name='com.kmxs.reader:id/title_tv', textMatches=r"^大家还在搜.*$"):
        if search_paths:
            # 若在当前页面检索到"停止条件"时, 并且存在未收集数据
            for _path in search_paths:
                # 获取小说信息
                title = _path.child("com.kmxs.reader:id/book_store_item_title").get_text()
                score = _path.child("com.kmxs.reader:id/tv_book_one_score").get_text()

                if title not in novels_list:
                    # 小说编号
                    novel_index += 1 
                    # 打印小说信息
                    print(f"Collect: No:{novel_index:03d}, name: {title:s}, score: {score:s}")
                    # 写入容器
                    novels_list.append(title)
        # 打印停止条件
        print(f"检测到文本：{stop_text_condition}, 停止向下滑动, 评分数据收集完毕")
        
        # 停止滑动页面，结束循环
        break
    else:
        # 若在当前页面检索到"停止条件"时(默认存在对应数据)，直接收集数据即可.
        for _path in search_paths:
            # 获取小说信息
            title = _path.child("com.kmxs.reader:id/book_store_item_title").get_text()
            score = _path.child("com.kmxs.reader:id/tv_book_one_score").get_text()

            if title not in novels_list:
                # 小说编号
                novel_index += 1 
                # 打印小说信息
                print(f"Collect: No:{novel_index:03d}, name: {title:s}, score: {score:s}")
                # 写入容器
                novels_list.append(title)
   
    # Step02: 继续屏幕滑动, 进行下一轮收集
    random_swipe_down()


# [3.5] 阅读小说(最后一本)详情，模仿翻页
read_novel = novels_list[-2]
poco(name='com.kmxs.reader:id/book_store_item_title', text=read_novel).click()


for page_no in range(1, 6):
    log(f"Read Novel Page - {page_no}: read content finished")
    # 模拟阅读页面
    sleep(global_random_delay)
    # 向左滑动页面(右上 -> 左下)
    poco.swipe([0.75, 0.65], [0.35, 0.75])

# 记录写进日志
log("Read novel page content finished, back to screen...")


# 课外补充: poco 常用控件方法 等待控件出现/消失
# poco("basic").wait_for_appearance()
# poco(text="Count").wait_for_disappearance(timeout=3)
# poco(name="").exists() 检测控件是否存在
# poco().wait_for_all() 在超时时长结束之前，需要 等待所有给定 UI对象/空间 显示出来
# poco().wait_for_any() 在超时时长结束之前，需要 等待任一给定 UI对象/空间 显示出来

# 模拟"后退操作", KEYNAME: HOME, BACK, KEYCODE_DEL, KEYCODE_MENU, KEYCODE_WAKEUP
keyevent("BACK")

# [3.6] 关闭 APP
sleep(global_random_delay)
stop_app(app_package)

# [3.7] 退回系统首页
sleep(global_random_delay)
keyevent("HOME")
